// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <lib/zx/timer.h>
#include <zircon/syscalls.h>
#include <zircon/time.h>
#include <zircon/types.h>

#include <dispatcher-pool/dispatcher-execution-domain.h>
#include <dispatcher-pool/dispatcher-timer.h>

#include <utility>

namespace dispatcher {

// static
fbl::RefPtr<Timer> Timer::Create(zx_time_t early_slop_nsec) {
  fbl::AllocChecker ac;

  auto ptr = new (&ac) Timer(early_slop_nsec);
  if (!ac.check())
    return nullptr;

  return fbl::AdoptRef(ptr);
}

zx_status_t Timer::Activate(fbl::RefPtr<ExecutionDomain> domain, ProcessHandler process_handler,
                            uint32_t slack_type) {
  if (process_handler == nullptr)
    return ZX_ERR_INVALID_ARGS;

  fbl::AutoLock obj_lock(&obj_lock_);
  if (is_active() || handle_.is_valid())
    return ZX_ERR_BAD_STATE;

  zx::timer timer;
  zx_status_t res = zx::timer::create(slack_type, ZX_CLOCK_MONOTONIC, &timer);
  if (res != ZX_OK)
    return res;

  // TODO(johngro): Set the early slop time on the timer.

  res = ActivateLocked(std::move(timer), std::move(domain));
  if (res != ZX_OK)
    return res;

  process_handler_ = std::move(process_handler);

  return ZX_OK;
}

void Timer::Deactivate() {
  ProcessHandler old_process_handler;

  {
    fbl::AutoLock obj_lock(&obj_lock_);
    DisarmLocked();
    InternalDeactivateLocked();

    // If we are in the process of actively dispatching, do not discard our
    // handler just yet.  It is currently being used by the dispatch thread.
    // Instead, wait until the dispatch thread unwinds and allow it to clean
    // up the handler.
    //
    // Otherwise, transfer the handler state into local storage and let it
    // destruct after we have released the object lock.
    if (dispatch_state() != DispatchState::Dispatching) {
      ZX_DEBUG_ASSERT((dispatch_state() == DispatchState::Idle) ||
                      (dispatch_state() == DispatchState::WaitingOnPort));
      old_process_handler = std::move(process_handler_);
    }
  }
}

zx_status_t Timer::Arm(zx_time_t deadline, zx_duration_t slack_amount) {
  fbl::AutoLock obj_lock(&obj_lock_);

  // If we are in the process of waiting on the port, or there is a dispatch
  // in flight, attempt to cancel the pending timer operation.
  if ((dispatch_state() == DispatchState::WaitingOnPort) ||
      (dispatch_state() == DispatchState::DispatchPending)) {
    CancelPendingLocked();
  }

  // Reset the armed state of the timer.
  DisarmLocked();

  // If we are no longer active, we cannot arm the timer.
  if (!is_active())
    return ZX_ERR_BAD_HANDLE;

  // If we are still active, we should still have a valid handle
  ZX_DEBUG_ASSERT(handle_.is_valid());

  // Record the current armed status.
  armed_ = true;
  deadline_ = deadline;
  slack_amount_ = slack_amount;

  // If we are currently Idle, set the timer and post a wait on our port.
  // Otherwise, there is a dispatch in flight that we failed to cancel.  The
  // timer will take appropriate action when the in-flight operation hits
  // Dispatch.
  if (dispatch_state() == DispatchState::Idle) {
    return SetTimerAndWaitLocked();
  }

  return ZX_OK;
}

void Timer::Cancel() {
  fbl::AutoLock obj_lock(&obj_lock_);

  // Disarm the timer and clear the internal bookkeeping.
  DisarmLocked();

  // If the timer handle has been closed, or the timer object is in the idle
  // state, or we are in the middle of a dispatch, then we are done.
  if (!handle_.is_valid() || (dispatch_state() == DispatchState::Idle) ||
      (dispatch_state() == DispatchState::Dispatching)) {
    return;
  }

  // We are either waiting on the port, or we are waiting in the dispatch
  // queue.  Attempt to cancel any pending dispatch operation.  It's OK if
  // this fails, it just means that the dispatch operation is in flight; we'll
  // figure it out by the time we hit Dispatch.
  ZX_DEBUG_ASSERT((dispatch_state() == DispatchState::WaitingOnPort) ||
                  (dispatch_state() == DispatchState::DispatchPending));
  if (dispatch_state() == DispatchState::WaitingOnPort)
    CancelPendingLocked();
}

void Timer::Dispatch(ExecutionDomain* domain) {
  ZX_DEBUG_ASSERT(domain != nullptr);
  ZX_DEBUG_ASSERT(process_handler_ != nullptr);

  // Check to make sure this timer should still fire.  It is possible that the
  // timer was canceled or changed at a point where the dispatch operation was
  // already in flight and could not be cancelled.
  bool do_dispatch;
  {
    fbl::AutoLock obj_lock(&obj_lock_);
    ZX_DEBUG_ASSERT(dispatch_state() == DispatchState::Dispatching);
    timer_set_ = false;

    // If we were disarmed, we are now back in the idle state
    if (!armed_) {
      dispatch_state_ = DispatchState::Idle;
      return;
    }

    // If the timer was moved into the future, skip the dispatch operation,
    // but fall into the code which re-sets the timer.  Otherwise, the timer
    // has now fired and we should reset our internal bookkeeping.
    do_dispatch = (zx_time_add_duration(zx_clock_get_monotonic(), early_slop_nsec_) >= deadline_);
    if (do_dispatch) {
      DisarmLocked();
    }
  }

  // Now, if we are actually supposed to dispatch this event, do so.
  zx_status_t res = do_dispatch ? process_handler_(this) : ZX_OK;

  // Finally, handle either re-arming the timer, or cleaning up as needed.
  ProcessHandler old_process_handler;
  {
    fbl::AutoLock obj_lock(&obj_lock_);
    ZX_DEBUG_ASSERT(dispatch_state() == DispatchState::Dispatching);
    dispatch_state_ = DispatchState::Idle;

    // Was there a problem during processing?  If so, make sure that we
    // de-activate ourselves.  Otherwise, if we are still active, and we are
    // supposed to be armed, attempt to set up the next wait-on-port
    // operation.
    if (res != ZX_OK) {
      InternalDeactivateLocked();
    } else {
      if (is_active() && armed_) {
        res = SetTimerAndWaitLocked();
        if (res != ZX_OK) {
          // TODO(johngro) : This should not fail, we should probably
          // log something instead of simply silently deactivating.
          InternalDeactivateLocked();
        }
      }
    }

    // Have we become deactivated (either during dispatching or just now)?
    // If so, move our process handler state outside of our lock so that it
    // can safely destruct.
    if (!is_active()) {
      old_process_handler = std::move(process_handler_);
    }
  }
}

void Timer::DisarmLocked() {
  // If the timer was set at the kernel level, cancel it.  No matter what, we
  // are now no longer armed.
  if (timer_set_ && handle_.is_valid()) {
    zx_timer_cancel(handle_.get());
  }
  timer_set_ = false;
  armed_ = false;
}

zx_status_t Timer::SetTimerAndWaitLocked() {
  ZX_DEBUG_ASSERT(armed_);

  zx_status_t res = zx_timer_set(handle_.get(), deadline_, slack_amount_);
  if (res != ZX_OK) {
    DisarmLocked();
    return res;
  }
  timer_set_ = true;

  res = WaitOnPortLocked();
  if (res != ZX_OK) {
    DisarmLocked();
  }

  return res;
}

}  // namespace dispatcher
